/*
 * Copyright 2009 Christian Schindelhauer, Peter Thiemann, Faisal Aslam, Luminous Fennell and Gidon Ernst.
 * All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 *
 * This code is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 3 for more details (a copy is
 * included in the LICENSE file that accompanied this code).
 *
 * You should have received a copy of the GNU General Public License
 * version 3 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA
 *
 * Please contact Faisal Aslam
 * (aslam AT informatik.uni-freibug.de or studentresearcher AT gmail.com)
 * if you need additional information or have any questions.
 */
package takatuka.offlineGC.DFA.logic.partialInstrOrder;

import java.util.*;
import takatuka.classreader.dataObjs.*;
import takatuka.classreader.logic.util.*;
import takatuka.offlineGC.DFA.dataObjs.*;
import takatuka.offlineGC.DFA.dataObjs.attribute.*;
import takatuka.offlineGC.DFA.dataObjs.flowRecord.*;
import takatuka.offlineGC.DFA.dataObjs.functionState.*;

import takatuka.optimizer.cpGlobalization.logic.util.*;
import takatuka.verifier.dataObjs.*;
import takatuka.verifier.dataObjs.attribute.*;
import takatuka.verifier.logic.factory.*;
import takatuka.verifier.logic.DFA.*;
import takatuka.vm.autoGenerated.forExceptionPrettyPrint.*;

/**
 * <p>Title: </p>
 * <p>Description:
 * A method was called before and has now finished its execution. However,
 * some field that is used inside that method is now changed. A the virtual interpeter
 * cannot order the execution of the methods hence recall that method again.
 *
 * </p>
 * @author Faisal Aslam
 * @version 1.0
 */
public class RecallAMethod {

    private static final RecallAMethod myObj = new RecallAMethod();
    private static Oracle oracle = Oracle.getInstanceOf();
    private static boolean shouldDebug = false;

    private static void debugPrint(Object obj1, Object obj2) {
        if (shouldDebug) {
            System.out.println(obj1 + ", " + obj2);
        }
    }

    /**
     * 
     */
    private RecallAMethod() {
    }

    /**
     * 
     * @return
     */
    public static RecallAMethod getInstanceOf() {
        return myObj;
    }

    public void execute() {
        OrderManagerForFieldInstrs orderMngForFieldInstrs = OrderManagerForFieldInstrs.getInstanceOf();
        HashMap<FunctionStateKey, FunctionStateKeyMapValue> fieldsToSetDirtyAfterMethodCalled =
                (HashMap<FunctionStateKey, FunctionStateKeyMapValue>) orderMngForFieldInstrs.getAfterCallMethodWithDirtyGetFieldInstr().clone();
        Set<FunctionStateKey> keysSet = fieldsToSetDirtyAfterMethodCalled.keySet();
        Iterator<FunctionStateKey> it = keysSet.iterator();

        while (it.hasNext()) {
            FunctionStateKey methodCallInfo = it.next();
            MethodInfo method = methodCallInfo.getMethod();
            String methodStr = oracle.getMethodOrFieldString(method);
            //System.out.println(methodStr);
            if (methodStr.contains("tryToSend")) {
                //DataFlowAnalyzer.shouldDebugPrint = true;
                //shouldDebug = true;
            }
            orderMngForFieldInstrs.removeForMapForDirtyGetFieldInstr(methodCallInfo);
            HashSet<FieldRecord> fieldRecSet = (HashSet<FieldRecord>) fieldsToSetDirtyAfterMethodCalled.get(methodCallInfo).getValue();
            if (fieldRecSet == null || fieldRecSet.size() == 0) {
                continue;
            }
            HashMap instrMap = reloadMethodState(methodCallInfo);
            makeSelectedInstrDirtyBasedOnFieldRecord(instrMap, fieldRecSet);
            callTheMethod(methodCallInfo);
        }
    }

    private void callTheMethod(FunctionStateKey methodCallInfo) {
        VerificationFrameFactory factory = VerificationPlaceHolder.getInstanceOf().getFactory();
        DataFlowAnalyzer dataFlowAnalyzer = factory.createDataFlowAnalyzer();
        Vector localVariables = methodCallInfo.getCallingParameters();
        GCOperandStack callerDummyOperandStack = (GCOperandStack) factory.createOperandStack(8);
        MethodInfo method = methodCallInfo.getMethod();

        Frame frame = InitializeFirstInstruction.createFrame(method, localVariables);
        BytecodeVerifier bcVeri = factory.createBytecodeVerifier(frame, method, localVariables);
        GCType oldRetunType = FunctionStateRecorder.getInstanceOf().getFunctionReturnType(method, localVariables);
        if (oldRetunType != null) {
            oldRetunType = (GCType) oldRetunType.clone();
        }
        ClassFile.currentClassToWorkOn = method.getClassFile();

        dataFlowAnalyzer.executeForce(method, localVariables, callerDummyOperandStack, bcVeri);

        checkIfParentsMethodNeededToBeRecalled(methodCallInfo, oldRetunType);
    }

    /**
     * check if any of the parents (of the method just invoke due to field change)
     * has to be invoked.
     *
     * @param methodCalled
     * @param oldReturnType
     */
    private void checkIfParentsMethodNeededToBeRecalled(FunctionStateKey methodCalled, GCType oldReturnType) {
        /**
         * If the method call returns nothing or does not return an object then we are done here.
         */
        if (oldReturnType == null || !oldReturnType.isReference()) {
            return;
        }
        FunctionStateRecorder stateRecorder = FunctionStateRecorder.getInstanceOf();

        GCType newRetType = stateRecorder.getFunctionReturnType(methodCalled.getMethod(), methodCalled.getCallingParameters());
        /**
         * new return type and old return type is same then also we are done here.
         */
        if (newRetType.equals(oldReturnType)) {
            return;
        }
        /**
         * By this point we are sure that method does return an object, and old
         * and new return types are not same. It implies that now we must call
         * all the parent methods as they will have different stack value.
         *
         * The following are the instructions that has invoked this method.
         */
        Collection<VerificationInstruction> instrInvokedTheMethod = MethodCallInfo.getFunAInvokedBy(methodCalled);
        /**
         * We need to do following.
         * 1) Get instrInvoked the method and get the corresponding method with all the calling parameters.
         * 2) Merge the stack of next instruction from invoke instruction and make it dirty if the stack is changed.
         * 3) Recall the method.
         */
        Iterator<VerificationInstruction> it = instrInvokedTheMethod.iterator();
        while (it.hasNext()) {
            VerificationInstruction instr = it.next();
            MethodInfo method = instr.getMethod();
            HashMap<FunctionStateKey, FunctionStateKeyMapValue> recordForMethodStates = stateRecorder.getFunctionStatePerMethod(method);
            Set<FunctionStateKey> keysSet = recordForMethodStates.keySet();
            Iterator<FunctionStateKey> keysIt = keysSet.iterator();
            GCInstruction nextInstr = getNextInstr((GCInstruction) instr);
            while (keysIt.hasNext()) {
                FunctionStateKey key = keysIt.next();
                HashMap instrMap = reloadMethodState(key);
                updateStackOfInstr(nextInstr, newRetType);
                /**
                 * making the next instr dirty.
                 */
                nextInstr.setChangeBit(true);

                callTheMethod(key);
            }
        }
    }

    private void updateStackOfInstr(GCInstruction nextInstr, GCType newReturnValue) {
        try {
            GCOperandStack stack = (GCOperandStack) nextInstr.getOperandStack();
            GCType type = (GCType) stack.pop();

            GCType mergedType = (GCType) stack.mergeReferences(type, newReturnValue);
            if (mergedType == null) {
                /**
                 * both type and newReturnValue are equal.
                 */
                mergedType = type;
            }
            stack.push(mergedType);
        } catch (Exception d) {
            d.printStackTrace();
            Miscellaneous.exit();
        }
    }

    private HashMap<Long, GCInstruction> reloadMethodState(FunctionStateKey methodCallInfo) {
        FunctionStateRecorder stateRecorder = FunctionStateRecorder.getInstanceOf();
        HashMap<Long, FunctionStateValueElement> stateInfo = stateRecorder.getFunctionState(methodCallInfo);
        MethodInfo methodInfo = methodCallInfo.getMethod();
        Vector instrVec = methodInfo.getInstructions();
        Iterator<GCInstruction> instrIt = instrVec.iterator();
        HashMap<Long, GCInstruction> instrMap = new HashMap<Long, GCInstruction>();
        while (instrIt.hasNext()) {
            GCInstruction instr = instrIt.next();
            instrMap.put(instr.getInstructionId(), instr);
            FunctionStateValueElement functStateElm = stateInfo.get(instr.getInstructionId());
            GCLocalVariables localVar = functStateElm.getGCLeavingLocalVariables();
            GCOperandStack operandStack = functStateElm.getGCEnteringOperandStack();
            if (localVar == null || operandStack == null) {
                continue;
            }
            instr.setChangeBit(false);
            instr.setVisited(true);
            debugPrint("\ninstr =", instr);
            debugPrint("stack=", operandStack);
            debugPrint("localVar=", localVar);

            instr.set(localVar, operandStack);
        }

        return instrMap;
    }

    private GCInstruction getNextInstr(GCInstruction valueReturnInstr) {
        MethodInfo method = valueReturnInstr.getMethod();
        Vector instrVec = method.getInstructions();
        Iterator<GCInstruction> instrIt = instrVec.iterator();
        while (instrIt.hasNext()) {
            GCInstruction instr = instrIt.next();
            if (instr.getInstructionId() == valueReturnInstr.getInstructionId()) {
                instr = instrIt.next();
                return instr;
            }
        }
        return null;
    }

    private void makeSelectedInstrDirtyBasedOnFieldRecord(HashMap<Long, GCInstruction> instrMap,
            HashSet<FieldRecord> fieldRecord) {
        Iterator<FieldRecord> fieldRecIt = fieldRecord.iterator();
        debugPrint("", "\n\n");
        while (fieldRecIt.hasNext()) {
            FieldRecord fieldRec = fieldRecIt.next();
            GCInstruction instr = fieldRec.getInstr();
            long instrId = instr.getInstructionId();
            int lineNumber = LineNumberController.getInstanceOf().getLineNumberInfo(instrId);
            String methodStr = oracle.getMethodOrFieldString(instr.getMethod());
            debugPrint("making dirty in ", lineNumber + ":" + methodStr);
            debugPrint("that is ", instr);
            instrMap.get(instrId).setChangeBit(true);
        }
    }
}
